/**
 * @author Kai Salmen / https://kaisalmen.de
 * Development repository: https://github.com/kaisalmen/WWOBJLoader
 */

import {
  DefaultLoadingManager,
  FileLoader,
  Group,
} from "../../../build/three.module.js";

import { OBJLoader2Parser } from "./obj2/worker/parallel/OBJLoader2Parser.js";
import { MeshReceiver } from "./obj2/shared/MeshReceiver.js";
import { MaterialHandler } from "./obj2/shared/MaterialHandler.js";

/**
 * Use this class to load OBJ data from files or to parse OBJ data from an arraybuffer
 * @class
 *
 * @param {DefaultLoadingManager} [manager] The loadingManager for the loader to use. Default is {@link DefaultLoadingManager}
 */
const OBJLoader2 = function (manager) {
  OBJLoader2Parser.call(this);
  this.manager =
    manager !== undefined && manager !== null ? manager : DefaultLoadingManager;

  this.modelName = "";
  this.instanceNo = 0;
  this.path = undefined;
  this.resourcePath = undefined;
  this.baseObject3d = new Group();

  this.materialHandler = new MaterialHandler();
  this.meshReceiver = new MeshReceiver(this.materialHandler);
};
OBJLoader2.OBJLOADER2_VERSION = "3.0.0";
console.info("Using OBJLoader2 version: " + OBJLoader2.OBJLOADER2_VERSION);

OBJLoader2.prototype = Object.create(OBJLoader2Parser.prototype);
OBJLoader2.prototype.constructor = OBJLoader2;

/**
 * Set the name of the model.
 *
 * @param {string} modelName
 * @return {OBJLoader2}
 */
OBJLoader2.prototype.setModelName = function (modelName) {
  this.modelName = modelName ? modelName : this.modelName;
  return this;
};

/**
 * The URL of the base path.
 *
 * @param {string} path URL
 * @return {OBJLoader2}
 */
OBJLoader2.prototype.setPath = function (path) {
  this.path = path ? path : this.path;
  return this;
};

/**
 * Allow to specify resourcePath for dependencies of specified resource.
 *
 * @param {string} resourcePath
 * @return {OBJLoader2}
 */
OBJLoader2.prototype.setResourcePath = function (resourcePath) {
  this.resourcePath = resourcePath ? resourcePath : this.resourcePath;
  return this;
};

/**
 * Set the node where the loaded objects will be attached directly.
 *
 * @param {Object3D} baseObject3d Object already attached to scenegraph where new meshes will be attached to
 * @return {OBJLoader2}
 */
OBJLoader2.prototype.setBaseObject3d = function (baseObject3d) {
  this.baseObject3d =
    baseObject3d === undefined || baseObject3d === null
      ? this.baseObject3d
      : baseObject3d;
  return this;
};

/**
 * Add materials as associated array.
 *
 * @param {Object} materials Object with named {@link Material}
 * @return {OBJLoader2}
 */
OBJLoader2.prototype.addMaterials = function (materials) {
  this.materialHandler.addMaterials(materials);
  return this;
};

/**
 * Register a function that is called once a single mesh is available and it could be altered by the supplied function.
 *
 * @param {Function} [onMeshAlter]
 * @return {OBJLoader2}
 */
OBJLoader2.prototype.setCallbackOnMeshAlter = function (onMeshAlter) {
  this.meshReceiver._setCallbacks(this.callbacks.onProgress, onMeshAlter);
  return this;
};

/**
 * Register a function that is called once all materials have been loaded and they could be altered by the supplied function.
 *
 * @param {Function} [onLoadMaterials]
 * @return {OBJLoader2}
 */
OBJLoader2.prototype.setCallbackOnLoadMaterials = function (onLoadMaterials) {
  this.materialHandler._setCallbacks(onLoadMaterials);
  return this;
};

/**
 * Use this convenient method to load a file at the given URL. By default the fileLoader uses an ArrayBuffer.
 *
 * @param {string}  url A string containing the path/URL of the file to be loaded.
 * @param {function} onLoad A function to be called after loading is successfully completed. The function receives loaded Object3D as an argument.
 * @param {function} [onFileLoadProgress] A function to be called while the loading is in progress. The argument will be the XMLHttpRequest instance, which contains total and Integer bytes.
 * @param {function} [onError] A function to be called if an error occurs during loading. The function receives the error as an argument.
 * @param {function} [onMeshAlter] Called after every single mesh is made available by the parser
 */
OBJLoader2.prototype.load = function (
  url,
  onLoad,
  onFileLoadProgress,
  onError,
  onMeshAlter
) {
  let scope = this;
  if (
    onLoad === null ||
    onLoad === undefined ||
    !(onLoad instanceof Function)
  ) {
    let errorMessage = "onLoad is not a function! Aborting...";
    scope.callbacks.onError(errorMessage);
    throw errorMessage;
  }
  if (
    onError === null ||
    onError === undefined ||
    !(onError instanceof Function)
  ) {
    onError = function (event) {
      let errorMessage = event;
      if (event.currentTarget && event.currentTarget.statusText !== null) {
        errorMessage =
          "Error occurred while downloading!\nurl: " +
          event.currentTarget.responseURL +
          "\nstatus: " +
          event.currentTarget.statusText;
      }
      scope.callbacks.onError(errorMessage);
    };
  }
  if (!url) {
    onError("An invalid url was provided. Unable to continue!");
  }
  let urlFull = new URL(url, window.location.href).href;
  let filename = urlFull;
  let urlParts = urlFull.split("/");
  if (urlParts.length > 2) {
    filename = urlParts[urlParts.length - 1];
    let urlPartsPath = urlParts.slice(0, urlParts.length - 1).join("/") + "/";
    if (urlPartsPath !== undefined && urlPartsPath !== null)
      this.path = urlPartsPath;
  }
  if (
    onFileLoadProgress === null ||
    onFileLoadProgress === undefined ||
    !(onFileLoadProgress instanceof Function)
  ) {
    let numericalValueRef = 0;
    let numericalValue = 0;
    onFileLoadProgress = function (event) {
      if (!event.lengthComputable) return;

      numericalValue = event.loaded / event.total;
      if (numericalValue > numericalValueRef) {
        numericalValueRef = numericalValue;
        let output =
          'Download of "' +
          url +
          '": ' +
          (numericalValue * 100).toFixed(2) +
          "%";
        scope.callbacks.onProgress("progressLoad", output, numericalValue);
      }
    };
  }

  this.setCallbackOnMeshAlter(onMeshAlter);
  let fileLoaderOnLoad = function (content) {
    onLoad(scope.parse(content));
  };
  let fileLoader = new FileLoader(this.manager);
  fileLoader.setPath(this.path || this.resourcePath);
  fileLoader.setResponseType("arraybuffer");
  fileLoader.load(filename, fileLoaderOnLoad, onFileLoadProgress, onError);
};

/**
 * Parses OBJ data synchronously from arraybuffer or string.
 *
 * @param {arraybuffer|string} content OBJ data as Uint8Array or String
 */
OBJLoader2.prototype.parse = function (content) {
  // fast-fail in case of illegal data
  if (content === null || content === undefined) {
    throw "Provided content is not a valid ArrayBuffer or String. Unable to continue parsing";
  }
  if (this.logging.enabled) {
    console.time("OBJLoader parse: " + this.modelName);
  }

  // sync code works directly on the material references
  this._setMaterials(this.materialHandler.getMaterials());

  if (content instanceof ArrayBuffer || content instanceof Uint8Array) {
    if (this.logging.enabled) console.info("Parsing arrayBuffer...");
    this.execute(content);
  } else if (typeof content === "string" || content instanceof String) {
    if (this.logging.enabled) console.info("Parsing text...");
    this.executeLegacy(content);
  } else {
    this.callbacks.onError(
      "Provided content was neither of type String nor Uint8Array! Aborting..."
    );
  }
  if (this.logging.enabled) {
    console.timeEnd("OBJLoader parse: " + this.modelName);
  }
  return this.baseObject3d;
};

OBJLoader2.prototype._onAssetAvailable = function (payload) {
  if (payload.cmd !== "assetAvailable") return;

  if (payload.type === "mesh") {
    let meshes = this.meshReceiver.buildMeshes(payload);
    for (let mesh of meshes) {
      this.baseObject3d.add(mesh);
    }
  } else if (payload.type === "material") {
    this.materialHandler.addPayloadMaterials(payload);
  }
};

export { OBJLoader2 };
